创建 user2 微服务
这一节的核心任务是在 Datacenter 2 中部署一个用户微服务实例,用于模拟异地容灾场景。最简单的方式是通过文件管理器复制现有 user 微服务项目(不建议在 VS Code 中复制,可能丢失 node_modules 等隐藏文件目录)。
修改服务配置
复制完成后需要修改三个关键配置:
1. 修改端口号(main.ts):
// user2/src/main.ts
await app.listen(40003); // 使用不同于 user1 的端口
typescript
2. 修改 Consul 注册地址:
// user2/src/consul/consul.client.service.ts
const consulClient = new Consul({
host: 'localhost',
port: 8501, // 注册到 Datacenter 2 的 Consul
});
typescript
3. 修改服务实例名称(区分不同实例):
// 将 serviceId 改为唯一的标识
const serviceId = 'nestjs-user-service-2'; // 原来是 nestjs-user-service-1
typescript
配置 pnpm workspace
将 user2 加入 workspace 配置,确保能正确引用共享模块:
# pnpm-workspace.yaml
packages:
- "apps/*"
- "libs/*"
- "health"
- "user2" # 新增
yaml
配置健康检查
更新 .env 文件
Health 服务的 .env 文件需要包含 user2 的健康检查端点:
# health/.env
CONSUL_HEALTH_CHECK_ENDPOINTS=http://localhost:40001/health,http://localhost:40002/health,http://localhost:40003/health
CONSUL_HEALTH_CHECK_KEYS=user1-grpc,user2-grpc,user2-grpc # 注意逗号分隔
PORT=3030
env
更新 Health Controller
// health/src/consul/consul.controller.ts
@Controller('health')
export class ConsulController {
@GrpcMethod('HealthService', 'Check')
async checkUser2() {
// 检查 user2 gRPC 服务状态
return { status: 'SERVING' };
}
}
typescript
服务注册与注销 API
Consul 提供了完整的 HTTP API 用于管理服务生命周期,在调试时非常实用。
查询已注册服务
# 查询 Datacenter 2 的所有服务
curl http://localhost:8501/v1/agent/services
bash
返回示例:
{
"nestjs-user-service-2": {
"ID": "nestjs-user-service-2",
"Service": "user-service",
"Tags": [],
"Address": "",
"Port": 40003,
"status": "passing"
}
}
json
注销服务(反注册)
# 注销指定服务
curl -X PUT http://localhost:8501/v1/agent/service/deregister/nestjs-user-service-2
bash
成功时返回 HTTP 200,无响应体。
清理脏数据的暴力方式
当 Consul 中的服务数据出现异常时,最彻底的清理方式:
# 1. 停止所有 Consul 容器
docker compose -f docker-compose.consul.yml down
# 2. 删除数据目录
rm -rf consul/data consul/data1
# 3. 重新启动
docker compose -f docker-compose.consul.yml up -d
bash
启动全流程
按以下顺序启动所有服务:
# 1. 启动 Consul Server(双数据中心)
docker compose -f docker-compose.consul.yml up -d
# 2. 启动用户微服务(dc1)
pnpm dev:user # 40001 端口
pnpm dev:user1 # 40002 端口
# 3. 启动用户微服务(dc2)
pnpm dev:user2 # 40003 端口
# 4. 启动健康检查服务
pnpm dev:health # 3030 端口
bash
验证测试
1. 验证服务注册
访问 Consul UI(http://localhost:8500),确认:
- dc1 中有 user-service-1 和 user-service-2 两个实例
- dc2 中有 user-service-2 一个实例
- 所有实例状态均为 passing(绿色)
2. 验证健康检查
# 检查 user2 的健康状态
curl http://localhost:3030/api/v1/health
bash
3. 验证故障感知
# 停止 user2 服务后立即检查
# Health 接口会很快返回 "server error"
curl http://localhost:3030/api/v1/health
# 但 Consul UI 上的状态更新会有延迟
# 这是因为 Consul 的健康检查有间隔周期
bash
Health Controller Scope 优化
为了在调试时获得更及时的健康状态反馈,建议将 Controller 的 Scope 设置为 REQUEST:
import { Controller, Scope } from '@nestjs/common';
@Controller({
path: 'health',
scope: Scope.REQUEST, // 每次请求创建新实例,避免缓存
})
export class HealthController {
constructor(
private readonly grpcClient: ClientGrpc, // 每次注入新实例
) {}
}
typescript
效果对比
| Scope 类型 | 行为 | 适用场景 |
|---|---|---|
DEFAULT(单例) | 实例缓存,状态可能滞后 | 生产环境(性能优先) |
REQUEST | 每次请求创建新实例,状态实时 | 开发调试(准确性优先) |
常见问题与解决
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 服务启动但未注册到 Consul | workspace 未配置 | 检查 pnpm-workspace.yaml |
| Consul UI 显示异常服务 | 旧数据未清理 | 删除 data 目录重新启动 |
| 健康检查一直显示 passing | Scope 缓存导致 | 改为 Scope.REQUEST |
| 跨 dc 查询返回空数据 | WAN 未配置 | 检查 retry_join_wan 配置 |
| 接口返回 404 | 路由未注册 | 检查 ConsulModule.forRoot() |
↑